/*
Copyright 2024 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package cmd

import (
	"go/ast"
	"go/token"
	"log"
	"os"
	"path/filepath"
	"reflect"
	"strings"
	"testing"

	"github.com/google/go-cmp/cmp"
	"k8s.io/apimachinery/pkg/util/version"
)

const expectedHeader = `# This file is generated by compatibility_lifecycle tool.
# Do not edit manually. Run hack/update-featuregates.sh to regenerate.

`

var testCurrentVersion = version.MustParse("1.32")

func TestVerifyAlphabeticOrder(t *testing.T) {
	tests := []struct {
		name      string
		keys      []string
		expectErr bool
	}{
		{
			name: "ordered versioned specs",
			keys: []string{
				"SchedulerQueueingHints", "SELinuxMount", "ServiceAccountTokenJTI",
				"genericfeatures.AdmissionWebhookMatchConditions",
				"genericfeatures.AggregatedDiscoveryEndpoint",
			},
		},
		{
			name: "unordered versioned specs",
			keys: []string{
				"SELinuxMount", "SchedulerQueueingHints", "ServiceAccountTokenJTI",
				"genericfeatures.AdmissionWebhookMatchConditions",
				"genericfeatures.AggregatedDiscoveryEndpoint",
			},
			expectErr: true,
		},
		{
			name: "unordered versioned specs with mixed pkg prefix",
			keys: []string{
				"genericfeatures.AdmissionWebhookMatchConditions",
				"SchedulerQueueingHints", "SELinuxMount", "ServiceAccountTokenJTI",
				"genericfeatures.AggregatedDiscoveryEndpoint",
			},
			expectErr: true,
		},
		{
			name: "unordered versioned specs with pkg prefix",
			keys: []string{
				"SchedulerQueueingHints", "SELinuxMount", "ServiceAccountTokenJTI",
				"genericfeatures.AggregatedDiscoveryEndpoint",
				"genericfeatures.AdmissionWebhookMatchConditions",
			},
			expectErr: true,
		},
	}
	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			err := verifyAlphabeticOrder(tc.keys, "")
			if tc.expectErr {
				if err == nil {
					t.Fatal("expected error, got nil")
				}
				return
			}
			if err != nil {
				t.Fatal(err)
			}
		})
	}
}

func TestVerifyOrUpdateFeatureListVersioned(t *testing.T) {
	featureListFileContent := expectedHeader + `- name: APIListChunking
  versionedSpecs:
  - default: true
    lockToDefault: true
    preRelease: GA
    version: "1.30"
- name: AppArmorFields
  versionedSpecs:
  - default: true
    lockToDefault: false
    preRelease: Beta
    version: "1.30"
- name: CPUCFSQuotaPeriod
  versionedSpecs:
  - default: false
    lockToDefault: false
    preRelease: Alpha
    version: "1.30"
  - default: true
    lockToDefault: false
    preRelease: Beta
    version: "1.31"
`
	tests := []struct {
		name                          string
		goFileContent                 string
		featureListFileContent        string
		updatedFeatureListFileContent string
		expectVerifyErr               bool
		expectUpdateErr               bool
	}{
		{
			name: "no change",
			goFileContent: `
package features

import (
	"k8s.io/apimachinery/pkg/util/version"
	"k8s.io/component-base/featuregate"
)
var defaultVersionedKubernetesFeatureGates = map[featuregate.Feature]featuregate.VersionedSpecs{
	AppArmorFields: {
		{Version: version.MajorMinor(1, 30), Default: true, PreRelease: featuregate.Beta},
	},
	CPUCFSQuotaPeriod: {
		{Version: version.MustParse("1.30"), Default: false, PreRelease: featuregate.Alpha},
		{Version: version.MustParse("1.31"), Default: true, PreRelease: featuregate.Beta},
	},
	genericfeatures.APIListChunking: {
		{Version: version.MustParse("1.30"), Default: true, PreRelease: featuregate.GA, LockToDefault: true},
	},
}
var otherFeatureGates = map[featuregate.Feature]featuregate.VersionedSpecs{
	AppArmorFields: {
		{Version: version.MajorMinor(1, 30), Default: true, PreRelease: featuregate.Beta},
	},
}
`,
			featureListFileContent:        featureListFileContent,
			updatedFeatureListFileContent: featureListFileContent,
		},
		{
			name: "semantically equivalent, formatting wrong",
			goFileContent: `
package features

import (
	"k8s.io/apimachinery/pkg/util/version"
	"k8s.io/component-base/featuregate"
)
var defaultVersionedKubernetesFeatureGates = map[featuregate.Feature]featuregate.VersionedSpecs{
	AppArmorFields: {
		{Version: version.MajorMinor(1, 30), Default: true, PreRelease: featuregate.Beta},
	},
	CPUCFSQuotaPeriod: {
		{Version: version.MustParse("1.30"), Default: false, PreRelease: featuregate.Alpha},
		{Version: version.MustParse("1.31"), Default: true, PreRelease: featuregate.Beta},
	},
	genericfeatures.APIListChunking: {
		{Version: version.MustParse("1.30"), Default: true, PreRelease: featuregate.GA, LockToDefault: true},
	},
}
var otherFeatureGates = map[featuregate.Feature]featuregate.VersionedSpecs{
	AppArmorFields: {
		{Version: version.MajorMinor(1, 30), Default: true, PreRelease: featuregate.Beta},
	},
}
`,
			featureListFileContent: expectedHeader + `- name: APIListChunking
  versionedSpecs:
    - default: true
      lockToDefault: true
      preRelease: GA
      version: "1.30"
- name: AppArmorFields
  versionedSpecs:
  - default: true
    lockToDefault: false
    preRelease: Beta
    version: "1.30"
- name: CPUCFSQuotaPeriod
  versionedSpecs:
  - default: false
    lockToDefault: false
    preRelease: Alpha
    version: "1.30"
  - default: true
    lockToDefault: false
    preRelease: Beta
    version: "1.31"
`,
			updatedFeatureListFileContent: featureListFileContent,
			expectVerifyErr:               true,
		},
		{
			name: "same feature added twice with different lifecycle",
			goFileContent: `
package features

import (
	"k8s.io/apimachinery/pkg/util/version"
	"k8s.io/component-base/featuregate"
)
var defaultVersionedKubernetesFeatureGates = map[featuregate.Feature]featuregate.VersionedSpecs{
	AppArmorFields: {
		{Version: version.MajorMinor(1, 30), Default: true, PreRelease: featuregate.Beta},
	},
	CPUCFSQuotaPeriod: {
		{Version: version.MustParse("1.30"), Default: false, PreRelease: featuregate.Alpha},
		{Version: version.MustParse("1.31"), Default: true, PreRelease: featuregate.Beta},
	},
	genericfeatures.APIListChunking: {
		{Version: version.MustParse("1.30"), Default: true, PreRelease: featuregate.GA, LockToDefault: true},
	},
}
var otherFeatureGates = map[featuregate.Feature]featuregate.VersionedSpecs{
	AppArmorFields: {
		{Version: version.MajorMinor(1, 30), Default: true, PreRelease: featuregate.Alpha},
	},
}
`,
			featureListFileContent: featureListFileContent,
			expectVerifyErr:        true,
			expectUpdateErr:        true,
		},
		{
			name: "VersionedSpecs not ordered by version",
			goFileContent: `
package features

import (
	"k8s.io/apimachinery/pkg/util/version"
	"k8s.io/component-base/featuregate"
)
var defaultVersionedKubernetesFeatureGates = map[featuregate.Feature]featuregate.VersionedSpecs{
	AppArmorFields: {
		{Version: version.MajorMinor(1, 30), Default: true, PreRelease: featuregate.Beta},
	},
	CPUCFSQuotaPeriod: {
		{Version: version.MustParse("1.31"), Default: false, PreRelease: featuregate.Alpha},
		{Version: version.MustParse("1.30"), Default: true, PreRelease: featuregate.Beta},
	},
	genericfeatures.APIListChunking: {
		{Version: version.MustParse("1.30"), Default: true, PreRelease: featuregate.GA, LockToDefault: true},
	},
}
`,
			featureListFileContent: featureListFileContent,
			expectVerifyErr:        true,
			expectUpdateErr:        true,
		},
		{
			name: "add new feature",
			goFileContent: `
package features

import (
	"k8s.io/apimachinery/pkg/util/version"
	"k8s.io/component-base/featuregate"
)
var defaultVersionedKubernetesFeatureGates = map[featuregate.Feature]featuregate.VersionedSpecs{
	AppArmorFields: {
		{Version: version.MajorMinor(1, 30), Default: true, PreRelease: featuregate.Beta},
	},
	ClusterTrustBundleProjection: {
		{Version: version.MajorMinor(1, 30), Default: true, PreRelease: featuregate.Beta},
	},
	CPUCFSQuotaPeriod: {
		{Version: version.MustParse("1.30"), Default: false, PreRelease: featuregate.Alpha},
		{Version: version.MustParse("1.31"), Default: true, PreRelease: featuregate.Beta},
	},
	genericfeatures.APIListChunking: {
		{Version: version.MustParse("1.30"), Default: true, PreRelease: featuregate.GA, LockToDefault: true},
	},
}
`,
			expectVerifyErr:        true,
			featureListFileContent: featureListFileContent,
			updatedFeatureListFileContent: expectedHeader + `- name: APIListChunking
  versionedSpecs:
  - default: true
    lockToDefault: true
    preRelease: GA
    version: "1.30"
- name: AppArmorFields
  versionedSpecs:
  - default: true
    lockToDefault: false
    preRelease: Beta
    version: "1.30"
- name: ClusterTrustBundleProjection
  versionedSpecs:
  - default: true
    lockToDefault: false
    preRelease: Beta
    version: "1.30"
- name: CPUCFSQuotaPeriod
  versionedSpecs:
  - default: false
    lockToDefault: false
    preRelease: Alpha
    version: "1.30"
  - default: true
    lockToDefault: false
    preRelease: Beta
    version: "1.31"
`,
		},
		{
			name: "not allowed to remove feature",
			goFileContent: `
package features

import (
	"k8s.io/apimachinery/pkg/util/version"
	"k8s.io/component-base/featuregate"
)
var defaultVersionedKubernetesFeatureGates = map[featuregate.Feature]featuregate.VersionedSpecs{
	CPUCFSQuotaPeriod: {
		{Version: version.MustParse("1.30"), Default: false, PreRelease: featuregate.Alpha},
		{Version: version.MustParse("1.31"), Default: true, PreRelease: featuregate.Beta},
	},
	genericfeatures.APIListChunking: {
		{Version: version.MustParse("1.30"), Default: true, PreRelease: featuregate.GA, LockToDefault: true},
	},
}
`,
			expectVerifyErr:        true,
			expectUpdateErr:        true,
			featureListFileContent: featureListFileContent,
			updatedFeatureListFileContent: expectedHeader + `- name: APIListChunking
  versionedSpecs:
  - default: true
    lockToDefault: true
    preRelease: GA
    version: "1.30"
- name: CPUCFSQuotaPeriod
  versionedSpecs:
  - default: false
    lockToDefault: false
    preRelease: Alpha
    version: "1.30"
  - default: true
    lockToDefault: false
    preRelease: Beta
    version: "1.31"
`,
		},
		{
			name: "update feature",
			goFileContent: `
package features

import (
	"k8s.io/apimachinery/pkg/util/version"
	"k8s.io/component-base/featuregate"
)
var defaultVersionedKubernetesFeatureGates = map[featuregate.Feature]featuregate.VersionedSpecs{
	AppArmorFields: {
		{Version: version.MajorMinor(1, 30), Default: true, PreRelease: featuregate.Beta},
	},
	CPUCFSQuotaPeriod: {
		{Version: version.MustParse("1.30"), Default: false, PreRelease: featuregate.Alpha},
		{Version: version.MustParse("1.31"), Default: true, PreRelease: featuregate.Beta},
		{Version: version.MustParse("1.32"), Default: true, PreRelease: featuregate.GA},
	},
	genericfeatures.APIListChunking: {
		{Version: version.MustParse("1.30"), Default: true, PreRelease: featuregate.GA, LockToDefault: true},
	},
}
`,
			expectVerifyErr:        true,
			featureListFileContent: featureListFileContent,
			updatedFeatureListFileContent: expectedHeader + `- name: APIListChunking
  versionedSpecs:
  - default: true
    lockToDefault: true
    preRelease: GA
    version: "1.30"
- name: AppArmorFields
  versionedSpecs:
  - default: true
    lockToDefault: false
    preRelease: Beta
    version: "1.30"
- name: CPUCFSQuotaPeriod
  versionedSpecs:
  - default: false
    lockToDefault: false
    preRelease: Alpha
    version: "1.30"
  - default: true
    lockToDefault: false
    preRelease: Beta
    version: "1.31"
  - default: true
    lockToDefault: false
    preRelease: GA
    version: "1.32"
`,
		},
	}
	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			featureListFile := writeContentToTmpFile(t, "", "feature_list.yaml", tc.featureListFileContent)
			tmpDir := filepath.Dir(featureListFile.Name())
			_ = writeContentToTmpFile(t, tmpDir, "pkg/new_features.go", tc.goFileContent)
			err := verifyOrUpdateFeatureList(tmpDir, filepath.Base(featureListFile.Name()), testCurrentVersion, false)
			if tc.expectVerifyErr != (err != nil) {
				t.Errorf("expectVerifyErr=%v, got err: %s", tc.expectVerifyErr, err)
			}
			err = verifyOrUpdateFeatureList(tmpDir, filepath.Base(featureListFile.Name()), testCurrentVersion, true)
			if tc.expectUpdateErr != (err != nil) {
				t.Errorf("expectUpdateErr=%v, got err: %s", tc.expectUpdateErr, err)
			}
			if tc.expectUpdateErr {
				return
			}
			updatedFeatureListFileContent, err := os.ReadFile(featureListFile.Name())
			if err != nil {
				t.Fatal(err)
			}
			if diff := cmp.Diff(string(updatedFeatureListFileContent), tc.updatedFeatureListFileContent); diff != "" {
				t.Errorf("updatedFeatureListFileContent does not match expected, diff=%s", diff)
			}
		})
	}
}

func TestExtractFeatureInfoListFromFileVersioned(t *testing.T) {
	tests := []struct {
		name             string
		fileContent      string
		expectedFeatures []featureInfo
		expectErr        bool
	}{
		{
			name: "map in var",
			fileContent: `
package features

import (
	"k8s.io/apimachinery/pkg/util/version"
	genericfeatures "k8s.io/apiserver/pkg/features"
	"k8s.io/component-base/featuregate"
)
var defaultVersionedKubernetesFeatureGates = map[featuregate.Feature]featuregate.VersionedSpecs{
	AppArmorFields: {
		{Version: version.MustParse("1.31"), Default: true, PreRelease: featuregate.Beta},
	},
	CPUCFSQuotaPeriod: {
		{Version: version.MustParse("1.29"), Default: false, PreRelease: featuregate.Alpha},
	},
	genericfeatures.AggregatedDiscoveryEndpoint: {
		{Version: version.MustParse("1.30"), Default: false, PreRelease: featuregate.Alpha},
	},
}
			`,
			expectedFeatures: []featureInfo{
				{
					Name:     "AppArmorFields",
					FullName: "AppArmorFields",
					VersionedSpecs: []featureSpec{
						{Default: true, PreRelease: "Beta", Version: "1.31"},
					},
				},
				{
					Name:     "CPUCFSQuotaPeriod",
					FullName: "CPUCFSQuotaPeriod",
					VersionedSpecs: []featureSpec{
						{Default: false, PreRelease: "Alpha", Version: "1.29"},
					},
				},
				{
					Name:     "AggregatedDiscoveryEndpoint",
					FullName: "genericfeatures.AggregatedDiscoveryEndpoint",
					VersionedSpecs: []featureSpec{
						{Default: false, PreRelease: "Alpha", Version: "1.30"},
					},
				},
			},
		},
		{
			name: "map in var with alias",
			fileContent: `
package features

import (
	"k8s.io/apimachinery/pkg/util/version"
	genericfeatures "k8s.io/apiserver/pkg/features"
	fg "k8s.io/component-base/featuregate"
)
const (
    CPUCFSQuotaPeriodDefault = false
)
var defaultVersionedKubernetesFeatureGates = map[fg.Feature]fg.VersionedSpecs{
	AppArmorFields: {
		{Version: version.MustParse("1.31"), Default: true, PreRelease: fg.Beta},
	},
	CPUCFSQuotaPeriod: {
		{Version: version.MustParse("1.29"), Default: false, PreRelease: fg.Alpha},
	},
	genericfeatures.AggregatedDiscoveryEndpoint: {
		{Version: version.MustParse("1.30"), Default: false, PreRelease: fg.Alpha},
	},
}
			`,
			expectedFeatures: []featureInfo{
				{
					Name:     "AppArmorFields",
					FullName: "AppArmorFields",
					VersionedSpecs: []featureSpec{
						{Default: true, PreRelease: "Beta", Version: "1.31"},
					},
				},
				{
					Name:     "CPUCFSQuotaPeriod",
					FullName: "CPUCFSQuotaPeriod",
					VersionedSpecs: []featureSpec{
						{Default: false, PreRelease: "Alpha", Version: "1.29"},
					},
				},
				{
					Name:     "AggregatedDiscoveryEndpoint",
					FullName: "genericfeatures.AggregatedDiscoveryEndpoint",
					VersionedSpecs: []featureSpec{
						{Default: false, PreRelease: "Alpha", Version: "1.30"},
					},
				},
			},
		},
		{
			name: "map in function return statement",
			fileContent: `
package features

import (
	"k8s.io/component-base/featuregate"
)

const (
	ComponentSLIs featuregate.Feature = "ComponentSLIs"
)

func featureGates() map[featuregate.Feature]featuregate.VersionedSpecs {
	return map[featuregate.Feature]featuregate.VersionedSpecs{
		ComponentSLIs: {
			{Version: version.MustParse("1.30"), Default: false, PreRelease: featuregate.Alpha},
			{Version: version.MustParse("1.31"), Default: true, PreRelease: featuregate.Beta},
			{Version: version.MustParse("1.32"), Default: true, PreRelease: featuregate.GA, LockToDefault: true},
		},
	}
}
			`,
			expectedFeatures: []featureInfo{
				{
					Name:     "ComponentSLIs",
					FullName: "ComponentSLIs",
					VersionedSpecs: []featureSpec{
						{Default: false, PreRelease: "Alpha", Version: "1.30"},
						{Default: true, PreRelease: "Beta", Version: "1.31"},
						{Default: true, PreRelease: "GA", Version: "1.32", LockToDefault: true},
					},
				},
			},
		},
		{
			name: "error when VersionedSpecs not ordered by version",
			fileContent: `
package features

import (
	"k8s.io/component-base/featuregate"
)

const (
	ComponentSLIs featuregate.Feature = "ComponentSLIs"
)

func featureGates() map[featuregate.Feature]featuregate.VersionedSpecs {
	return map[featuregate.Feature]featuregate.VersionedSpecs{
		ComponentSLIs: {
			{Version: version.MustParse("1.30"), Default: false, PreRelease: featuregate.Alpha},
			{Version: version.MustParse("1.32"), Default: true, PreRelease: featuregate.GA, LockToDefault: true},
			{Version: version.MustParse("1.31"), Default: true, PreRelease: featuregate.Beta},
		},
	}
}
			`,
			expectErr: true,
		},
	}
	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			newFile := writeContentToTmpFile(t, "", "new_features.go", tc.fileContent)
			fset := token.NewFileSet()
			features, err := extractFeatureInfoListFromFile(fset, newFile.Name())
			if tc.expectErr {
				if err == nil {
					t.Fatal("expect err")
				}
				return
			}
			if err != nil {
				t.Fatal(err)
			}
			if diff := cmp.Diff(features, tc.expectedFeatures); diff != "" {
				t.Errorf("File contents: got=%v, want=%v, diff=%s", features, tc.expectedFeatures, diff)
			}
		})
	}
}

func writeContentToTmpFile(t *testing.T, tmpDir, fileName, fileContent string) *os.File {
	if tmpDir == "" {
		p, err := os.MkdirTemp("", "k8s")
		if err != nil {
			t.Fatal(err)
		}
		tmpDir = p
	}
	fullPath := filepath.Join(tmpDir, fileName)
	err := os.MkdirAll(filepath.Dir(fullPath), os.ModePerm)
	if err != nil {
		t.Fatal(err)
	}
	tmpfile, err := os.Create(fullPath)
	if err != nil {
		log.Fatal(err)
	}
	_, err = tmpfile.WriteString(fileContent)
	if err != nil {
		t.Fatal(err)
	}
	err = tmpfile.Close()
	if err != nil {
		t.Fatal(err)
	}
	return tmpfile
}

func TestParseFeatureSpec(t *testing.T) {
	tests := []struct {
		name                string
		val                 ast.Expr
		expectedFeatureSpec featureSpec
	}{
		{
			name: "spec by field name",
			expectedFeatureSpec: featureSpec{
				Default: true, LockToDefault: true, PreRelease: "Beta", Version: "1.31",
			},
			val: &ast.CompositeLit{
				Elts: []ast.Expr{
					&ast.KeyValueExpr{
						Key: &ast.Ident{
							Name: "Version",
						},
						Value: &ast.CallExpr{
							Fun: &ast.SelectorExpr{
								X: &ast.Ident{
									Name: "version",
								},
								Sel: &ast.Ident{
									Name: "MustParse",
								},
							},
							Args: []ast.Expr{
								&ast.BasicLit{
									Kind:  token.STRING,
									Value: "\"1.31\"",
								},
							},
						},
					},
					&ast.KeyValueExpr{
						Key: &ast.Ident{
							Name: "Default",
						},
						Value: &ast.Ident{
							Name: "true",
						},
					},
					&ast.KeyValueExpr{
						Key: &ast.Ident{
							Name: "LockToDefault",
						},
						Value: &ast.Ident{
							Name: "true",
						},
					},
					&ast.KeyValueExpr{
						Key: &ast.Ident{
							Name: "PreRelease",
						},
						Value: &ast.SelectorExpr{
							X: &ast.Ident{
								Name: "featuregate",
							},
							Sel: &ast.Ident{
								Name: "Beta",
							},
						},
					},
				},
			},
		},
	}
	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			variables := map[string]ast.Expr{}
			spec, err := parseFeatureSpec(variables, tc.val)
			if err != nil {
				t.Fatal(err)
			}
			if !reflect.DeepEqual(tc.expectedFeatureSpec, spec) {
				t.Errorf("expected: %#v, got %#v", tc.expectedFeatureSpec, spec)
			}
		})
	}
}
func TestVerifyFeatureRemoval(t *testing.T) {
	tests := []struct {
		name             string
		featureList      []featureInfo
		baseFeatureList  []featureInfo
		currentVersion   *version.Version
		thresholdVersion *version.Version
		expectErr        bool
		expectedErrMsg   string
	}{
		{
			name: "no features removed",
			featureList: []featureInfo{
				{Name: "FeatureA", VersionedSpecs: []featureSpec{{Version: "1.0", PreRelease: "Alpha"}}},
				{Name: "FeatureB", VersionedSpecs: []featureSpec{{Version: "1.0", PreRelease: "Beta"}}},
			},
			baseFeatureList: []featureInfo{
				{Name: "FeatureA", VersionedSpecs: []featureSpec{{Version: "1.0", PreRelease: "Alpha"}}},
				{Name: "FeatureB", VersionedSpecs: []featureSpec{{Version: "1.0", PreRelease: "Beta"}}},
			},
			currentVersion: version.MustParse("1.1"),
			expectErr:      false,
		},
		{
			name: "alpha feature removed",
			featureList: []featureInfo{
				{Name: "FeatureB", VersionedSpecs: []featureSpec{{Version: "1.0", PreRelease: "Beta"}}},
			},
			baseFeatureList: []featureInfo{
				{Name: "FeatureA", VersionedSpecs: []featureSpec{{Version: "1.0", PreRelease: "Alpha"}}},
				{Name: "FeatureB", VersionedSpecs: []featureSpec{{Version: "1.0", PreRelease: "Beta"}}},
			},
			currentVersion: version.MustParse("1.1"),
			expectErr:      false,
		},
		{
			name: "beta feature removed",
			featureList: []featureInfo{
				{Name: "FeatureA", VersionedSpecs: []featureSpec{{Version: "1.0", PreRelease: "Alpha"}}},
			},
			baseFeatureList: []featureInfo{
				{Name: "FeatureA", VersionedSpecs: []featureSpec{{Version: "1.0", PreRelease: "Alpha"}}},
				{Name: "FeatureB", VersionedSpecs: []featureSpec{{Version: "1.0", PreRelease: "Beta"}}},
			},
			currentVersion: version.MustParse("1.1"),
			expectErr:      true,
			expectedErrMsg: "feature FeatureB cannot be removed while in beta",
		},
		{
			name: "GA feature removed before allowed version",
			featureList: []featureInfo{
				{Name: "FeatureA", VersionedSpecs: []featureSpec{{Version: "1.0", PreRelease: "Alpha"}}},
			},
			baseFeatureList: []featureInfo{
				{Name: "FeatureA", VersionedSpecs: []featureSpec{{Version: "1.0", PreRelease: "Alpha"}}},
				{Name: "FeatureC", VersionedSpecs: []featureSpec{{Version: "1.0", PreRelease: "GA", LockToDefault: true}}},
			},
			currentVersion: version.MustParse("1.2"),
			expectErr:      true,
			expectedErrMsg: "feature FeatureC cannot be removed until version 1.3 (required for emulation support)",
		},
		{
			name: "GA feature removed after allowed version",
			featureList: []featureInfo{
				{Name: "FeatureA", VersionedSpecs: []featureSpec{{Version: "1.0", PreRelease: "Alpha"}}},
			},
			baseFeatureList: []featureInfo{
				{Name: "FeatureA", VersionedSpecs: []featureSpec{{Version: "1.0", PreRelease: "Alpha"}}},
				{Name: "FeatureC", VersionedSpecs: []featureSpec{{Version: "1.0", PreRelease: "GA", LockToDefault: true}}},
			},
			currentVersion: version.MustParse("1.4"),
			expectErr:      false,
		},
		{
			name: "feature with no version specs",
			featureList: []featureInfo{
				{Name: "FeatureA", VersionedSpecs: []featureSpec{{Version: "1.0", PreRelease: "Alpha"}}},
			},
			baseFeatureList: []featureInfo{
				{Name: "FeatureA", VersionedSpecs: []featureSpec{{Version: "1.0", PreRelease: "Alpha"}}},
				{Name: "FeatureD", VersionedSpecs: []featureSpec{}},
			},
			currentVersion: version.MustParse("1.1"),
			expectErr:      true,
			expectedErrMsg: "feature FeatureD has no version specs",
		},
		{
			name: "feature with invalid version",
			featureList: []featureInfo{
				{Name: "FeatureA", VersionedSpecs: []featureSpec{{Version: "1.0", PreRelease: "Alpha"}}},
			},
			baseFeatureList: []featureInfo{
				{Name: "FeatureA", VersionedSpecs: []featureSpec{{Version: "1.0", PreRelease: "Alpha"}}},
				{Name: "FeatureE", VersionedSpecs: []featureSpec{{Version: "invalid", PreRelease: "GA", LockToDefault: true}}},
			},
			currentVersion: version.MustParse("1.1"),
			expectErr:      true,
			expectedErrMsg: "invalid version \"invalid\" for feature FeatureE",
		},
		{
			name: "GA feature not locked to default",
			featureList: []featureInfo{
				{Name: "FeatureA", VersionedSpecs: []featureSpec{{Version: "1.0", PreRelease: "Alpha"}}},
			},
			baseFeatureList: []featureInfo{
				{Name: "FeatureA", VersionedSpecs: []featureSpec{{Version: "1.0", PreRelease: "Alpha"}}},
				{Name: "FeatureC", VersionedSpecs: []featureSpec{{Version: "1.0", PreRelease: "GA"}}},
			},
			currentVersion: version.MustParse("1.4"),
			expectErr:      true,
			expectedErrMsg: "feature FeatureC cannot be removed because it is in GA or Deprecated state and is not locked to default",
		},
		{
			name: "Deprecated feature not locked to default",
			featureList: []featureInfo{
				{Name: "FeatureA", VersionedSpecs: []featureSpec{{Version: "1.0", PreRelease: "Alpha"}}},
			},
			baseFeatureList: []featureInfo{
				{Name: "FeatureA", VersionedSpecs: []featureSpec{{Version: "1.0", PreRelease: "Alpha"}}},
				{Name: "FeatureD", VersionedSpecs: []featureSpec{{Version: "1.0", PreRelease: "Deprecated"}}},
			},
			currentVersion: version.MustParse("1.4"),
			expectErr:      true,
			expectedErrMsg: "feature FeatureD cannot be removed because it is in GA or Deprecated state and is not locked to default",
		},
		{
			name: "GA feature removed at threshold version",
			featureList: []featureInfo{
				{Name: "FeatureA", VersionedSpecs: []featureSpec{{Version: "1.0", PreRelease: "Alpha"}}},
			},
			baseFeatureList: []featureInfo{
				{Name: "FeatureA", VersionedSpecs: []featureSpec{{Version: "1.0", PreRelease: "Alpha"}}},
				{Name: "FeatureC", VersionedSpecs: []featureSpec{{Version: "1.4", PreRelease: "GA", LockToDefault: true}}},
			},
			currentVersion:   version.MustParse("1.5"),
			thresholdVersion: version.MustParse("1.4"),
			expectErr:        false,
		},
	}
	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			err := verifyFeatureRemoval(tc.featureList, tc.baseFeatureList, tc.currentVersion, tc.thresholdVersion)
			if tc.expectErr {
				if err == nil {
					t.Fatalf("expected error, got nil")
				}
				if !strings.Contains(err.Error(), tc.expectedErrMsg) {
					t.Fatalf("expected error message to contain %q, got %q", tc.expectedErrMsg, err.Error())
				}
				return
			}
			if err != nil {
				t.Fatalf("unexpected error: %v", err)
			}
		})
	}
}

func TestVerifyAlphaFeatures(t *testing.T) {
	tests := []struct {
		name           string
		featureList    []featureInfo
		expectErr      bool
		expectedErrMsg string
	}{
		{
			name: "no alpha features",
			featureList: []featureInfo{
				{Name: "FeatureB", VersionedSpecs: []featureSpec{{Version: "1.0", PreRelease: "Beta"}}},
				{Name: "FeatureC", VersionedSpecs: []featureSpec{{Version: "1.0", PreRelease: "GA", LockToDefault: true}}},
			},
		},
		{
			name: "alpha feature disabled",
			featureList: []featureInfo{
				{Name: "FeatureA", VersionedSpecs: []featureSpec{{Version: "1.0", PreRelease: "Alpha", Default: false}}},
				{Name: "FeatureB", VersionedSpecs: []featureSpec{{Version: "1.0", PreRelease: "Beta"}}},
			},
		},
		{
			name: "alpha feature enabled",
			featureList: []featureInfo{
				{Name: "FeatureA", VersionedSpecs: []featureSpec{{Version: "1.0", PreRelease: "Alpha", Default: true}}},
				{Name: "FeatureB", VersionedSpecs: []featureSpec{{Version: "1.0", PreRelease: "Beta"}}},
			},
			expectErr:      true,
			expectedErrMsg: "alpha feature FeatureA cannot be enabled by default",
		},
	}
	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			err := verifyAlphaFeatures(tc.featureList)
			if tc.expectErr {
				if err == nil {
					t.Fatalf("expected error, got nil")
				}
				if !strings.Contains(err.Error(), tc.expectedErrMsg) {
					t.Fatalf("expected error message to contain %q, got %q", tc.expectedErrMsg, err.Error())
				}
				return
			}
			if err != nil {
				t.Fatalf("unexpected error: %v", err)
			}
		})
	}
}
